<?php

namespace tmcore\services\addons;

use Throwable;
use think\Response;
use think\facade\Config;
use think\exception\HttpResponseException;
use tmcore\utils\File;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Response as WorkResponse;
use Workerman\Protocols\Http\ServerSentEvents;

/**
 * 执行命令终端服务类
 */
class TerminalService extends BaseService
{
    /**
     * @var ?TerminalService 对象实例
     */
    protected static ?TerminalService $instance = null;

    /**
     * @var string 当前执行的命令参数
     */
    protected string $commandParams = '';

    /**
     * @var TcpConnection
     */
    protected $connection = null;

    /**
     * @var array
     */
    protected array $connectionParams = [];

    /**
     * @var string 扩展参数
     */
    protected string $extendParams = '';

    /**
     * @var string 命令执行UUID
     */
    protected string $commandUuid = '';

    /**
     * @var int 执行状态:0=未执行,1=执行中,2=执行完毕
     */
    protected int $execStatus = 0;

    /**
     * @var array 执行状态数据
     */
    protected array $execData = [];

    /**
     * @var string 命令执行输出文件
     */
    protected string $outputFile = '';

    /**
     * @var string 命令执行实时输出内容
     */
    protected string $outputContent = '';

    /**
     * 返回实例
     *
     * @return TerminalService 返回当前类的实例
     */
    public static function instance(): TerminalService
    {
        if (is_null(self::$instance)) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    /**
     * 初始化终端日志文件
     */
    public function initLog()
    {
        // 日志路径
        $outputDir = $this->root_path . 'runtime' . DIRECTORY_SEPARATOR . 'terminal';
        $this->outputFile = $outputDir . DIRECTORY_SEPARATOR . 'exec.log';
        // 如果输出目录不存在，则创建
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }
        // 清空输出文件内容
        file_put_contents($this->outputFile, '');
    }

    /**
     * 初始化参数
     *
     * @param $params
     * @return void
     */
    public function initParams($params)
    {
        // 从请求参数中执行的命令
        $this->commandParams = $params['command'] ?? '';
        // 从请求参数中获取扩展信息
        $this->extendParams = $params['extend'] ?? '';
        // 从请求参数中获取命令执行UUID
        $this->commandUuid = $params['uuid'] ?? '';
    }

    /**
     * 构造函数
     */
    public function __construct()
    {
        parent::__construct();

        $params = request()->param();
        // 初始化参数
        $this->initParams($params);
        // 初始化日志文件
        $this->initLog();
    }

    /**
     * 设置连接
     *
     * @param TcpConnection $connection
     * @return TerminalService|null
     */
    public function setConnection(TcpConnection $connection)
    {
        $this->connection = $connection;
        return self::$instance;
    }

    /**
     * 设置连接参数
     *
     * @param array $params
     * @return TerminalService|null
     */
    public function setConnectionParams(array $params)
    {
        $this->connectionParams = $params;
        $this->initParams($params);
        return self::$instance;
    }

     /**
     * 检查命令环境是否满足要求
     *
     * @return bool 返回是否满足要求
     */
    public function checkCommandEnv(): bool
    {
        if($this->connection && request()->isCli()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 获取命令信息
     *
     * @param string $params 命令参数
     * @return array|bool 返回命令信息数组或false
     */
    public function getCommandInfo(string $params)
    {
        if (!$params) return false;

        // 获取插件终端命令
        $commands = Config::get('addons.terminal_commands');

        // 如果命令包含点号，分割命令
        if (stripos($params, '.')) {
            $key_arr = explode('.', $params);

            if(!array_key_exists($key_arr[0], $commands)) return false;

            if((!empty($key_arr[1])) && (!array_key_exists($key_arr[1], $commands[$key_arr[0]]))) return false;

            if((!empty($key_arr[2])) && (!array_key_exists($key_arr[2], $commands[$key_arr[0]][$key_arr[1]]))) return false;

            if(empty($key_arr[2])) {
                $result = [
                    'command' => $commands[$key_arr[0]][$key_arr[1]],
                    'dir' => $this->sys_path . (empty($commands[$key_arr[0]]['dir'])?'':$commands[$key_arr[0]]['dir'])
                ];
            } else {
                $result = [
                    'command' => $commands[$key_arr[0]][$key_arr[1]][$key_arr[2]],
                    'dir' => $this->sys_path . (empty($commands[$key_arr[0]][$key_arr[1]]['dir'])?'':$commands[$key_arr[0]][$key_arr[1]]['dir'])
                ];
            }
        } else {
            if (!array_key_exists($params, $commands)) {
                return false;
            }
            $result = [
                'command' => $commands[$params],
                'dir' => $this->sys_path
            ];
        }
        // 返回命令和执行目录
        return $result;
    }

    /**
     * 设置响应头
     */
    public function setHeader()
    {
        $sys_header = Config::get('cookie.header');
        $headers = array_merge($sys_header ?? [], [
            'X-Accel-Buffering' => 'no',
            'Content-Type' => 'text/event-stream',
            'Cache-Control' => 'no-cache',
        ]);

        if($this->checkCommandEnv()) {
            $this->connection->send(new WorkResponse(200, $headers, "\r\n"));
        } else {
            // 设置响应头
            foreach ($headers as $name => $val) {
                header($name . (!is_null($val) ? ':' . $val : ''));
            }
        }
    }

    /**
     * 执行命令
     *
     * @throws Throwable 如果执行过程中出现异常
     */
    public function execute(): void
    {
        // 设置响应头
        $this->setHeader();

        // 清空输出缓冲区
        while (ob_get_level()) {
            ob_end_clean();
        }
        // 开启新的输出缓冲区
        if (!ob_get_level()) ob_start();

        set_time_limit(0);

        try {
            
            // 获取要执行的命令
            $info = $this->getCommandInfo($this->commandParams);
            if (!$info) {
                // 如果命令不存在，输出错误信息并终止执行
                throw new \Exception('找不到执行命令'. $this->commandParams);
            } else {
                // 检查命令类
                if(!(!empty($info['command']) && is_string($info['command']) && $info['command'])) {
                    throw new \Exception('命令类型异常');
                }
            }

            // 输出连接成功的标志
            $this->output('命令连接成功...', 'link_success');

            // 输出要执行的命令
            $this->output('> ' . $info['command'], 'exec_info');

            /**
             * 定义进程描述符
             * 注意:输出到管道时有延迟,而文件输出实时
             */
            $descriptorSpec = [
                0 => ['pipe', 'r'], // 标准输入
                1 => ['file', $this->outputFile, 'w'], // 标准输出
                2 => ['file', $this->outputFile, 'w'] // 标准错误
            ];

            // 获取环境变量
            $env = $this->getTerminalEnv($info['command']);

            // 打开一个进程执行命令
            $process = proc_open($info['command'], $descriptorSpec, $pipes, $info['dir'], $env);
            if (!is_resource($process)) {
                // 如果进程打开失败，输出错误信息并终止执行
                throw new \Exception('命令执行失败');
            }

            // 循环检查进程状态
            while ($this->getExecStatus($process)) {
                // 读取输出文件内容
                $contents = file_get_contents($this->outputFile);
                if (strlen($contents) && $this->outputContent != $contents) {
                    // 提取新的输出内容
                    $newOutput = str_replace($this->outputContent, '', $contents);
                    // 检查输出内容
                    $this->checkOutput($contents, $newOutput);
                    if (preg_match('/\r\n|\r|\n/', $newOutput)) {
                        // 输出新的内容
                        $this->output($newOutput, 'exec_info');
                        // 更新已输出的内容
                        $this->outputContent = $contents;
                    }
                }

                // 输出执行状态信息
                if ($this->execStatus === 2) {
                    if ($this->execData['exitcode'] === 0) {
                        // 成功执行后回调
                        $res = $this->successCallback();
                        if ($res) {
                            // 如果回调成功，输出执行成功
                            $this->output('命令执行成功!', 'exec_success');
                        } else {
                            $this->output('Error:命令执行失败', 'exec_info');
                        }
                    } else {
                        // 如果退出码不为0，输出错误
                        $this->output('Error: 命令执行失败,退出状态码为'. $this->execData['exitcode'], 'exec_info');
                    }
                }

                // 暂停0.5秒
                usleep(500000);
            }
            // 关闭所有管道
            foreach ($pipes as $pipe) {
                fclose($pipe);
            }
            // 关闭进程
            proc_close($process);
            // 输出执行完成的标志
            $this->output('命令执行完成!', 'exec_complete');

        } catch (\Exception $e) {
            // 捕获异常并输出错误信息
            $this->throwExecException($e->getMessage());
        }
    }


    /**
     * 获取终端环境变量
     *
     * @param $command
     * @return array|null
     */
    protected function getTerminalEnv($command)
    {
        $env = null;
        $commands = Config::get('addons.terminal_commands');

        // 检验是否包含 composer 命令
        if(strpos($command, 'composer') !== false) {
            $env = [];
            if(!empty($commands['env']['path'])) $env['PATH'] = $commands['env']['path'];
            if(!empty($commands['env']['composer_home'])) $env['COMPOSER_HOME'] = $commands['env']['composer_home'];
        }

        return $env;
    }

    /**
     * 获取执行状态
     *
     * @throws Throwable 如果获取状态过程中出现异常
     * @return bool 如果进程正在运行或刚结束返回true，否则返回false
     */
    public function getExecStatus($process): bool
    {
        // 获取进程状态
        $this->execData = proc_get_status($process);
        if ($this->execData['running']) {
            // 如果进程正在运行，标记为执行中
            $this->execStatus = 1;
            return true;
        } elseif ($this->execStatus === 1) {
            // 如果进程刚结束，标记为执行完毕
            $this->execStatus = 2;
            return true;
        } else {
            return false;
        }
    }

    /**
     * 输出 EventSource 数据
     *
     * @param string $data 要输出的数据
     * @param string $type 要输出的类型
     */
    public function output(string $data, string $type): void
    {
        // 过滤输出数据
        $data = self::outputFilter($data);
        $data = [
            'data' => $data,
            'type' => $type,
            'uuid' => $this->commandUuid,
            'extend' => $this->extendParams,
            'command' => $this->commandParams,
        ];
        // 将数据编码为JSON字符串
        $data = json_encode($data, JSON_UNESCAPED_UNICODE);
        if ($data) {
            if($this->checkCommandEnv()) {
                $this->connection->send(new ServerSentEvents(['event' => 'message', 'data' => $data]));
            } else {
                // 最终输出数据
                echo 'data: ' . $data . "\n\n";
                @ob_flush();// 刷新浏览器缓冲区
            }
        }
    }

    /**
     * 检查输出
     *
     * @param string $outputs 全部输出内容
     * @param string $rowOutput 当前输出内容（行）
     * @throws \Exception
     */
    public function checkOutput(string $outputs, string $rowOutput): void
    {
        if (str_contains($rowOutput, '(Y/n)')) {
            // 如果输出包含交互提示，输出错误信息并终止执行
            throw new \Exception('检测到交互输出，请手动执行命令确认情况');
        }
    }

    /**
     * 成功后回调
     *
     * @param string $commandParams 命令参数
     * @return bool 如果回调执行成功返回true，否则返回false
     * @throws Throwable 如果回调过程中出现异常
     */
    public function successCallback($commandParams = ''): bool
    {
        if($commandParams) $this->commandParams = $commandParams;

        $result = UtilService::getBuildDirs($this->commandParams);
        if($result) {
            return $this->moveDistFile($result['command_arr'][0], $result['dist_file_arr'], $result['to_file_arr']);
        }

        return true;
    }

    /**
     * 移动编译文件
     *
     * @param string $module 模块名
     * @param array $dist_file_arr 编译文件目录
     * @param array $to_file_arr 目标文件目录
     * @return bool
     */
    public function moveDistFile($module, $dist_file_arr, $to_file_arr)
    {
        // 检测编译文件
        foreach ($dist_file_arr as $file) {
            if (!file_exists($file)) {
                $this->output('Error:' . '编译文件不存在,文件路径:'. str_replace($this->sys_path, '', $file), 'exec_info');
                return true;
            }
        }

        try {
            // 删除公共编译目录文件
            foreach ($to_file_arr as $file) {
                if(is_dir($file)) {
                    File::del_file($file);
                } else {
                    if(file_exists($file)) @unlink($file);
                }
            }

            for($i = 0; $i < count($to_file_arr); $i++) {
                // 转移文件
                if(!File::recursiveMove($dist_file_arr[$i], $to_file_arr[$i])) {
                    throw new \Exception('转移编译文件失败,文件路径:'. str_replace($this->root_path, '', $to_file_arr[$i]));
                }
            }

            // 删除编译目录
            File::del_file($this->sys_path . $module. '/dist');

            return true;
        } catch (\Exception $e) {
            // 异常时还原备份的前端编译文件
            $backup_dir = $this->root_path . 'data/backup' . DIRECTORY_SEPARATOR;
            if($module == 'uniapp') $module = 'app';
            if (is_dir($backup_dir . $module)) {
                File::copy_dir($backup_dir . $module, $this->root_path . 'public/' . $module);
            }
            $this->output('Error:' . $e->getMessage(), 'exec_info');
            return true;
        }
    }

    /**
     * 输出过滤
     *
     * @param mixed $str 要过滤的字符串
     * @return string 过滤后的字符串
     */
    public static function outputFilter($str): string
    {
        // 去除首尾空格
        $str = trim($str);
        // 去除ANSI转义序列
        $preg = '/\[(.*?)m/i';
        $str = preg_replace($preg, '', $str);
        // 统一换行符
        $str = str_replace(["\r\n", "\r", "\n"], "\n", $str);
        // 转换编码为UTF-8
        return mb_convert_encoding($str, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
    }

    /**
     * 执行错误
     *
     * @param $msg
     */
    public function throwExecException($msg)
    {
        // 输出错误信息
        $this->output('Error:' . $msg, 'exec_error');
        if($this->checkCommandEnv()) return;
        // 抛出异常,退出执行
        throw new HttpResponseException(Response::create()->contentType('text/event-stream'));
    }

    /**
     * 执行命令获得字符串结果
     *
     * @param array $commandInfo 命令信息
     * @return array|false
     */
    public function getExecResult($commandInfo = [])
    {
        // 获取命令信息
        if($commandInfo) {
            if(empty($commandInfo['command']) || empty($commandInfo['dir'])) return false;
            $info = $commandInfo;
        } else {
            $info = $this->getCommandInfo($this->commandParams);
            if (!$info) return false;
        }

        // 定义进程描述符
        $descriptorsPec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
        // 获取环境变量
        $env = $this->getTerminalEnv($info['command']);

        // 打开一个进程执行命令
        $process = proc_open($info['command'], $descriptorsPec, $pipes, $info['dir'], $env);

        if (is_resource($process)) {
            try {
                // 读取标准输出和错误输出
                $result = stream_get_contents($pipes[1]);
                $result .= stream_get_contents($pipes[2]);
                // 关闭管道
                fclose($pipes[1]);
                fclose($pipes[2]);
                $status = proc_get_status($process);
                // 关闭进程
                proc_close($process);
                // 过滤输出信息
                return [
                    'status' => $status,
                    'result' => self::outputFilter($result),
                ];
            } catch (\Exception $e) {
                return [
                    'status' => -1,
                    'result' => $e->getMessage(),
                ];
            }
        }

        return false;
    }

}